package nmea0183

import (
	"errors"
	"fmt"
	"strconv"
	"strings"

	"github.com/project-flogo/core/data/metadata"

	"github.com/project-flogo/core/activity"
	"github.com/project-flogo/core/data"
	"github.com/project-flogo/core/data/coerce"
	_ "github.com/project-flogo/core/data/expression/script"
	"github.com/project-flogo/core/data/mapper"
	"github.com/project-flogo/core/data/resolve"

	nmea "github.com/masterclock/go-nmea"
)

type Activity struct {
	verify bool
}

func init() {
	_ = activity.Register(&Activity{}, New)
}

func New(ctx activity.InitContext) (activity.Activity, error) {
	var settings Settings
	err := metadata.MapToStruct(ctx.Settings(), settings, true)
	if err != nil {
		return nil, err
	}
	act := &Activity{
		verify: !settings.SkipVerify,
	}
	return act, nil
}

var activityMd = activity.ToMetadata(&Input{}, &Output{}, &Settings{})

func (a *Activity) Metadata() *activity.Metadata {
	return activityMd
}

func (a *Activity) Eval(ctx activity.Context) (bool, error) {
	input := Input{}
	err := ctx.GetInputObject(&input)
	if err != nil {
		return true, err
	}
	if a.verify {
		if err := verifySentence(input.Sentence); err != nil {
			return true, err
		}
	}
	var data map[string]interface{}
	// var err error
	switch strings.ToUpper(input.Type) {
	case "EXPR":
		{
			data, err = evalExpr(ctx, &input)
		}
	case "NMEA":
		{
			data, err = evalNMEA(ctx, &input)
		}
	default:
		{
			data, err = evalFields(ctx, &input)
		}
	}

	errStr := ""
	if err != nil {
		errStr = err.Error()
	}
	output := Output{
		Data: data,
		Err:  errStr,
	}
	err = ctx.SetOutputObject(&output)
	return true, err
}

func evalExpr(ctx activity.Context, input *Input) (map[string]interface{}, error) {
	mapping := map[string]interface{}{
		"data": input.Fields,
	}
	f := mapper.NewFactory(resolve.GetBasicResolver())
	aMapper, err := f.NewMapper(mapping)
	if err != nil {
		return map[string]interface{}{}, err
	}
	scope := data.NewSimpleScope(input.ToMap(), nil)
	results, err := aMapper.Apply(scope)
	if err != nil {
		return map[string]interface{}{}, err
	}
	data, err := coerce.ToObject(results["data"])
	if err != nil {
		return map[string]interface{}{}, err
	}
	return data, nil
}

func evalNMEA(ctx activity.Context, input *Input) (map[string]interface{}, error) {
	sentence := input.Sentence
	sentence = strings.TrimSpace(sentence)
	if sentence == "" {
		ctx.Logger().Warn("failed parse empty sentence")
		return map[string]interface{}{}, nil
	}
	indexStart := strings.Index(sentence, "$")
	if indexStart < 0 {
		ctx.Logger().Errorf("sentence not contains start char, sentence = %s", sentence)
		return map[string]interface{}{}, nil
	}
	indexEnd := strings.IndexAny(sentence, "\r\n")
	if indexEnd < 0 {
		indexEnd = len(sentence)
	}
	sentence = sentence[indexStart:indexEnd]
	s, err := nmea.Parse(sentence)
	if err != nil {
		ctx.Logger().Errorf("failed parse sentence = %s, err = %v", input.Sentence, err)
		return map[string]interface{}{}, err
	}
	m, err := s.ToMap()
	if err != nil {
		ctx.Logger().Errorf("failed convert to map, err = %v ", err)
		return map[string]interface{}{}, err
	}
	mv := map[string]interface{}{}
	for k, v := range m {
		name := fmt.Sprintf("nmea.%s.%s", strings.ToLower(s.Prefix()), k)
		mv[name] = v
	}
	return mv, nil
}

func evalFields(ctx activity.Context, input *Input) (map[string]interface{}, error) {
	fields := splitSentence(input.Fields)
	fs := splitSentence(input.Sentence)
	if len(fs) != len(fields) {
		return map[string]interface{}{}, fmt.Errorf("unmatch sentence: %s", input.Sentence)
	}
	data := map[string]interface{}{}
	for idx, name := range fields {
		if name != "" && name != "_" {
			data[name] = fs[idx]
		}
	}
	return data, nil
}

func verifySentence(str string) error {
	str = strings.TrimSpace(str)
	if len(str) < 4 {
		return errors.New("sentence too short")
	}
	if str[0] != '$' {
		return fmt.Errorf("sentence must start with $, got %v", str[0])
	}
	parts := strings.Split(str, "*")
	if len(parts) != 2 {
		return fmt.Errorf("sentence must contains 1 *")
	}
	if len(parts[1]) != 2 {
		return fmt.Errorf("sentence must have 1 byte checksum, got: %s", parts[1])
	}
	checksum := XorCheck([]byte(parts[0][1:]))
	checksumGot, err := toByte(parts[1])
	if err != nil {
		return fmt.Errorf("sentence checksum error: %v", err)
	}
	if checksum != checksumGot {
		return fmt.Errorf("sentence payload expect checksum to be %02X, got %02X", checksum, checksumGot)
	}
	return nil
}

func toByte(s string) (byte, error) {
	v, err := strconv.ParseUint(s, 16, 8)
	return byte(v), err
}

func splitSentence(str string) []string {
	fs := strings.Split(str, ",")
	if len(fs) > 0 {
		last := fs[len(fs)-1]
		part := strings.Split(last, "*")
		fs = append(fs[:len(fs)-1], part...)
	}
	return fs
}
